/*
 * File:   threaded_server.c
 * Author: onteria_
 *
 * Created on April 30, 2011, 7:11 AM
 */

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

#include <unicode/ustdio.h>

#include <miko/defines.h>
#include <miko/socket.h>
#include <miko/protocol.h>
#include <miko/miko.h>

#include "threaded_server.h"
#include "config_file.h"

/**
 * Function: check_socket_error
 *
 * A function to produce human readable error message for defined errors in <miko/defines.h>.
 * It also closes the socket and terminates the thread.
 *
 * Parameters:
 *
 *      error_code - The error code to print the readable error message for
 *      thread - The thread to kill
 *      file_descriptor - The file descriptor of the socket to close
 *
 * Returns:
 *
 *      No return
 */
void check_socket_error(int error_code, pthread_t thread, int file_descriptor) {
	const char * error_message = miko_get_error_string(error_code);
	fprintf(stderr, "ERROR %s\n", error_message);
    close(file_descriptor);
    pthread_cancel(thread);
}

/**
 * Function: miko_create_server
 * 
 * Create a multi-threaded server given a port and host
 * 
 * Parameters:
 * 
 *      port - The port to listen on
 *      host - The address to bind to
 *      max_connections - The maximum number of connections to listen for
 *      ipv6 - Whether or not to enable ipv6 support
 * 
 * Returns:
 *      
 *      The file descriptor of the server, or -1 if there was a failure
 */
int miko_create_server(int port, const char * host, int max_connections, int ipv6) {
    int server;
    int listen_result;
    
    if(max_connections <= 0) {
    	fprintf(stderr, "[ERROR] invalid number of connections given.\n");
    	return -1;
    }

    if(ipv6) {
    	server = socket(PF_INET6, SOCK_STREAM, 0);
    }
    else {
    	server = socket(PF_INET, SOCK_STREAM, 0);
    }
    if(server < 0) {
        perror("[ERROR] cannot create socket ");
        return MIKO_ERROR;
    }
    
    if(ipv6) {
    	struct sockaddr_in6 servAddr;
    	servAddr = miko_create_socket6(port, host);

    	if(servAddr.sin6_port == 0) {
    		return MIKO_ERROR;
    	}

    	if(bind(server, (struct sockaddr *) &servAddr, sizeof(struct sockaddr_in6)) < 0) {
    		perror("[ERROR] cannot bind port ");
    		return MIKO_ERROR;
    	}
    }
    else {
    	struct sockaddr_in servAddr;
    	servAddr = miko_create_socket(port, host);

        if(servAddr.sin_port == 0) {
            return -1;
        }

        if(bind(server, (struct sockaddr *) &servAddr, sizeof(struct sockaddr_in)) < 0) {
            perror("[ERROR] cannot bind port ");
            return MIKO_ERROR;
        }
    }

    listen_result = listen(server, max_connections);
    if(listen_result < 0) {
    	perror("[ERROR] server cannot listen ");
    	close(server);
    	return MIKO_ERROR;
    }
    
    return server;
}

/**
 * Function: miko_setup_server
 * 
 * A function to setup servers depending on the IPV settings and report any errors
 * 
 * Parameters:
 * 
 *      ipv6 - This is set to MIKO_IPV4 for ipv4 only MIKO_IPV6 for ipv6 only MIKO_DUAL_IPV for both
 * 
 * Returns:
 *
 * 		0 on success, -1 if there was an error setting up one of the servers.
 *
 */
int miko_setup_server(int ipv6) {
	/* TODO: Make a function that accepts a pointer to config_file and parses all the options instead of this madness */
    switch(ipv6) {
    case MIKO_IPV4:
    	server_ipv4 = miko_create_server(config_file.port_ipv4, config_file.server_ipv4, config_file.total_ipv4_connections, MIKO_IPV4);
    	if(server_ipv4 < 0) {
    		fprintf(stderr, "ERROR Could not create IPV4 server: %s [%d] %d connections max.\n",
    				config_file.server_ipv4, config_file.port_ipv4, config_file.total_ipv4_connections);
    		return -1;
    	}
    	break;
    case MIKO_IPV6:
    	server_ipv6 = miko_create_server(config_file.port_ipv6, config_file.server_ipv6, config_file.total_ipv6_connections, MIKO_IPV6);
    	if(server_ipv6 < 0) {
    		fprintf(stderr, "ERROR Could not create IPV6 server: %s [%d] %d connections max.\n",
    				config_file.server_ipv6, config_file.port_ipv6, config_file.total_ipv6_connections);
    		return -1;
    	}
    	break;
    case MIKO_DUAL_IPV:
    	server_ipv4 = miko_create_server(config_file.port_ipv4, config_file.server_ipv4, config_file.total_ipv4_connections, MIKO_IPV4);
    	if(server_ipv4 < 0) {
    		fprintf(stderr, "ERROR Could not create IPV4 server: %s [%d] %d connections max.\n",
    				config_file.server_ipv4, config_file.port_ipv4, config_file.total_ipv4_connections);
    		return -1;
    	}
    	server_ipv6 = miko_create_server(config_file.port_ipv6, config_file.server_ipv6, config_file.total_ipv6_connections, MIKO_IPV6);
    	if(server_ipv6 < 0) {
    		fprintf(stderr, "ERROR Could not create IPV6 server: %s [%d] %d connections max.\n",
    				config_file.server_ipv6, config_file.port_ipv6, config_file.total_ipv6_connections);
    		return -1;
    	}
    	break;
    }

    return 0;
}

/**
 * Function: miko_run_server
 *
 * Function to handle the acceptance of clients and thread spawning
 *
 * Parameters:
 *
 *      server - The file descriptor of the server
 *      handler - The function to handle each client request
 *
 * Returns:
 *      
 *      0 for success or -1 if there was a failure
 */
int miko_run_server(int server, void *(*handler)(void*), int ipv6) {
    int client;
    socklen_t addr_len = 0;
    pthread_t thread;

    /* We need to set the socket to non-blocking so that dual IPV4/6 bind works correctly */
	if (fcntl(server, F_SETFL, O_NDELAY) < 0) {
		perror("[ERROR] Can't set socket to non-blocking");
		return -1;
	}

	if(ipv6) {
		struct sockaddr_in6 cliAddr = {0};
		addr_len = sizeof(cliAddr);
		client = accept(server, (struct sockaddr *) &cliAddr, &addr_len);
	}
	else {
		struct sockaddr_in cliAddr = {0};   /* socket address for client */
		addr_len = sizeof(cliAddr);
		client = accept(server, (struct sockaddr *) &cliAddr, &addr_len);
	}

	if(client > 0) {
		pthread_create(&thread, 0, handler, (void *)&client);
		/* Release resources after the thread exits */
		pthread_detach(thread);
	}
    
    return 0;
}

/**
 * Function: process_connection
 * 
 * Handle the processing of the individual client connection from a
 * multi threaded server
 * 
 * Parameters:
 * 
 *      local_socket - The socket of the client connection
 * 
 * Returns:
 *      
 *      No value is returned
 */
void *process_connection(void * local_socket) {
    int client_local;
    int result;

    struct Miko miko;
    /* TODO: This needs to be a #define somewhere in <miko/defines.h> */
    char message[MIKO_MSG_LENGTH] = "Miko received okay";

    UFILE *out;
    
    client_local = *((int *)local_socket);
    bzero(&miko, sizeof(struct Miko));

    result = process_miko_data(client_local, &miko, config_file.timeout);
    if(result < 0) {
    	check_socket_error(result, pthread_self(), client_local);
    } else {
		/* TODO: Make this a general function */
		out = u_finit(stdout, NULL, NULL);
		u_fprintf(out, "Miko Data: [First Name] %S [Last Name] %S [Shrine] %S\n", &miko.firstName, &miko.lastName, &miko.shrine);
		u_fclose(out);

		result = miko_send_response(client_local, 200, message);
		if(result < 0) {
			check_socket_error(result, pthread_self(), client_local);
		}
    }

	/* Otherwise gcc barks at us for not returning something for void* */
	return NULL;
}
